package de.tud.kom.socom.web.client.graphview; import static de.tud.kom.socom.web.client.graphview.GlobalGraphSettings.*; import java.util.HashMap; import java.util.Map; import org.sgx.raphael4gwt.raphael.Paper; import org.sgx.raphael4gwt.raphael.Raphael; import org.sgx.raphael4gwt.raphael.Rect; import org.sgx.raphael4gwt.raphael.Set; import org.sgx.raphael4gwt.raphael.Shape; import org.sgx.raphael4gwt.raphael.base.Attrs; import org.sgx.raphael4gwt.raphael.base.Glow; import org.sgx.raphael4gwt.raphael.base.Rectangle; import org.sgx.raphael4gwt.raphael.event.Callback; import org.sgx.raphael4gwt.raphael.event.DDListener; import org.sgx.raphael4gwt.raphael.event.HoverListener; import org.sgx.raphael4gwt.raphael.event.MouseEventListener; import org.sgx.raphael4gwt.raphael.widget.PaperWidget; import com.google.gwt.dom.client.NativeEvent; import com.google.gwt.json.client.JSONArray; import com.google.gwt.json.client.JSONObject; import com.google.gwt.user.client.Timer; import de.tud.kom.socom.web.client.GraphPanel; import de.tud.kom.socom.web.client.Helper; import de.tud.kom.socom.web.client.drawables.DrawableConnection; import de.tud.kom.socom.web.client.drawables.DrawableNode; public abstract class AbstractGraph extends PaperWidget { protected Paper paper; protected JSONObject graph; protected Map<Long, Integer> levels; //a level is a vertical space in the graph (a vertical line could describe a level) protected Map<Long, DrawableNode> drawables; private Map<Integer, Integer> nodesAtLevel; private int maxLevel; private boolean concistent; private int dx; private DDListener dragL; private Set optionSet; protected long[] nodes; protected long[] endNodes; protected long startNode, centralNode; private Rect fullpaper; public AbstractGraph(JSONObject graph, boolean zoomable, boolean dynamicNodesize, long center){ super(PAPER_WIDTH, PAPER_HEIGTH); this.paper = this.getPaper(); this.graph = graph; this.nodes = getNodes(); this.startNode = (long)graph.get("startnode").isNumber().doubleValue(); if(startNode == -1){ paper.text(PAPER_WIDTH/2, PAPER_HEIGTH/5, "The Graph is empty. (Startnode: " + startNode + ")"); return; } this.endNodes = Helper.asArray(graph.get("endnodes").isArray()); this.centralNode = center; GlobalGraphSettings.TIME_SPENT_AVG_MIN = (long)graph.get("timeSpentAvgMin").isNumber().doubleValue(); GlobalGraphSettings.TIME_SPENT_AVG_MAX = (long)graph.get("timeSpentAvgMax").isNumber().doubleValue(); this.levels = getNodeLevel(); if(levels == null) { concistent = false; showConsistencyError(); return; } concistent = true; this.drawables = new HashMap<Long, DrawableNode>(); this.nodesAtLevel = new HashMap<Integer, Integer>(); init(); if(zoomable) addZoomOption(); if(dynamicNodesize) setDynamicNodeSize(); } private long[] getNodes() { JSONArray contexts = graph.get("contexts").isArray(); long[] nodes = new long[contexts.size()]; for(int i = 0; i < contexts.size(); i++) { JSONObject contextobject = contexts.get(i).isObject(); nodes[i] = (long)(contextobject.get("id").isNumber().doubleValue()); } return nodes; } abstract Map<Long, Integer> getNodeLevel(); private void init() { maxLevel = 0; for(Long n : levels.keySet()){ Integer level = levels.get(n); nodesAtLevel.put(level, nodesAtLevel.containsKey(level)?nodesAtLevel.get(level)+1:1); if(maxLevel < level) maxLevel = level; } int w = PAPER_WIDTH - NODES_OFFSET_LEFT - NODES_OFFSET_RIGHT; dx = w / maxLevel; // paper.rect(0,0,PAPER_WIDTH,PAPER_HEIGTH); //debug: show canvas limits } private void addZoomOption() { final Rect fullpaper = getFullpaperRect(); fullpaper.drag(dragL = new DDListener() { private Rect selection; private int startX, startY; @Override public void onStart(int x, int y, NativeEvent e) { startX = x - PAPER_OFFSET_X; startY = y - PAPER_OFFSET_Y; selection = paper.rect(startX, startY, 4, 4, 0); selection.attr("stroke-color", NODE_STROKE_COLOR).attr("fill", NODE_COLOR).attr("opacity", 0.3); ANIMATION_BUSY = true; } @Override public void onMove(int dx, int dy, int x, int y, NativeEvent e) { int currentX = x - PAPER_OFFSET_X; // 10 corrention for cursor int currentY = y - PAPER_OFFSET_Y; int w = currentX - startX; int h = currentY - startY; if(w < 0){ selection.attr("x", currentX).attr("width", w * -1); } else { selection.attr("x", startX).attr("width", w); } if(h < 0) { selection.attr("y", currentY).attr("height", h * -1); } else { selection.attr("y", startY).attr("height", h); } } @Override public void onEnd(NativeEvent e) { Rectangle box = selection.getBBox(); if(box.getHeight() * box.getWidth() > (paper.getHeight() * paper.getHeight() / 30)) { // only zoom bigger areas CURRENT_ZOOM = getDiag(box.getWidth(),box.getHeight())/getDiag(paper.getWidth(),paper.getHeight()); zoomTo(fullpaper, box);//box.getX(), box.getY(), box.getWidth(), box.getHeight()); } selection.remove(); selection = null; ANIMATION_BUSY = false; } },25); } public Rect getFullpaperRect() { if(fullpaper == null) { fullpaper = paper.rect(0,0,PAPER_WIDTH,PAPER_HEIGTH); fullpaper.attr("fill", "#000").attr("opacity", 0); } return fullpaper; } protected double getDiag(double width, double height) { double xq = width*width; double yq = height*height; return Math.sqrt(xq+yq); } protected void zoomTo(Rect fullpaper, final Rectangle box) { fullpaper.undrag(); // disable further zooming // setViewBoxAnimated(box); fadeOut(new Callback() { public void call(Shape src) { double originalRatio = (double)PAPER_WIDTH / (double)PAPER_HEIGTH; double newRatio = box.getWidth() / box.getHeight(); double zoom; if(newRatio > originalRatio){ //width ausschlaggebend zoom = PAPER_WIDTH/box.getWidth(); } else { //height ausschlaggebend zoom = PAPER_HEIGTH/box.getHeight(); } CURRENT_ZOOM = zoom; paper.setViewBox(box, true); fadeIn(); src.hide(); } }); drawOptionBox(true); } @SuppressWarnings("unused") @Deprecated /*not smooth :(*/ private void setViewBoxAnimated(Rectangle box) { final double x1,y1,w1,h1; x1=0; y1=0; w1=paper.getWidth(); h1=paper.getHeight(); final double x2,y2,w2,h2; x2=box.getX(); y2=box.getY(); w2=box.getWidth(); h2=box.getHeight(); double steps = 25; for(double step = 1; step <= steps; step++) { final double factor = step/steps; new Timer() { @Override public void run() { int x = (int) ((1-factor)*x1 + factor*x2); int y = (int) ((1-factor)*y1 + factor*y2); int w = (int) ((1-factor)*w1 + factor*w2); int h = (int) ((1-factor)*h1 + factor*h2); paper.setViewBox((int)x, (int)y, (int)w, (int)h, true); } }.schedule((int) (100*5*(step/steps))); } } public void drawOptionBox(boolean paint) { if(!paint) return; int w = GlobalGraphSettings.OPTION_BOX_WIDTH; int h = GlobalGraphSettings.OPTION_BOX_HEIGHT; // Paper optionPaper = Raphael.paper(PAPER_OFFSET_X,PAPER_OFFSET_Y,w,h); PaperWidget pW = new PaperWidget(w, h); Paper optionPaper = pW.getPaper(); GraphPanel.get(-1, -1).addPanel(pW); Shape r1 = optionPaper.rect(0,0,w,h,0).attr("stroke-color", "#333").attr("fill","#fff"); Shape r2 = optionPaper.rect(5,5,h-10,h-10,0); //from http://raphaeljs.com/icons Shape p1 = optionPaper.path("M29.772,26.433l-7.126-7.126c0.96-1.583,1.523-3.435,1.524-5.421C24.169,8.093,19.478,3.401,13.688,3.399C7.897,3.401,3.204,8.093,3.204,13.885c0,5.789,4.693,10.481,10.484,10.481c1.987,0,3.839-0.563,5.422-1.523l7.128,7.127L29.772,26.433zM7.203,13.885c0.006-3.582,2.903-6.478,6.484-6.486c3.579,0.008,6.478,2.904,6.484,6.486c-0.007,3.58-2.905,6.476-6.484,6.484C10.106,20.361,7.209,17.465,7.203,13.885z"); p1.transform("s0.6t-2,-2"); Shape t1 = optionPaper.text(0, h/2, "Reset Zoom"); t1.transform("t" + (h+t1.getBBox().getWidth()/2) + ",0"); final Rect hoverRect = paper.rect(0,0,w,h,0); hoverRect.attr("fill", "#f00").attr("opacity", 0); final Set glowSet = Raphael.set(optionPaper, r1, r2, p1); if(optionSet!=null)optionSet.remove(); optionSet = Raphael.set(optionPaper, glowSet, t1, hoverRect); final HoverListener hoverL = new HoverListener() { private Set g; @Override public void hoverOut(NativeEvent e) { if (g != null) { g.remove(); g = null; } } @Override public void hoverIn(NativeEvent e) { g = glowSet.glow(new Glow(5, false, 0.7, 0, 0, "#999")); } }; optionSet.click(new MouseEventListener() { public void notifyMouseEvent(NativeEvent e) { optionSet.unhover(hoverL); hoverL.hoverOut(null); optionSet.remove(); fadeOut(new Callback() { public void call(Shape src) { resetViewbox(getFullpaperRect()); src.hide(); fadeIn(); } }); } }); optionSet.hover(hoverL); } public void resetViewbox(Rect fullpaper) { CURRENT_ZOOM = 1; paper.setViewBox(fullpaper.getBBox(), true); fullpaper.drag(dragL, 25); } private void setDynamicNodeSize() { int nc = nodes.length; int mediumNC = 15; if(nc > mediumNC){ int rx = NODE_WIDTH; int ry = NODE_HEIGHT; int fontsize = NODE_TEXT_SIZE; int factor = nc/mediumNC; DYNAMIC_NODE_WIDTH = rx / factor; DYNAMIC_NODE_HEIGHT = ry / factor; DYNAMIC_NODE_TEXT_SIZE = fontsize / factor; } } public void paint(boolean paint, int nodeTextYOffset){ if(!paint)return; if(!concistent) return; int[] alreadyPutOnLevel = new int[maxLevel + 1]; for(long n : nodes) { Integer level = levels.get(n); if(level == null) continue; //skip nodes w/o level (e.g. if subgraph) int x = NODES_OFFSET_LEFT + dx * level; int y = NODES_OFFSET_TOP + (alreadyPutOnLevel[level]+1)*(PAPER_HEIGTH - NODES_OFFSET_TOP - NODES_OFFSET_BOTTOM)/(nodesAtLevel.get(level)+1); alreadyPutOnLevel[level]++; JSONObject contectdata = Helper.findContextData(graph.get("contexts").isArray(), n); DrawableNode dnode = new DrawableNode(contectdata); drawables.put(n, dnode); dnode.paint(paper, x, y, nodeTextYOffset); JSONArray relationsTo = contectdata.get("relationsTo").isArray(); for(int i = 0; i < relationsTo.size(); i++){ long timesUsed = (long) relationsTo.get(i).isObject().get("timesUsed").isNumber().doubleValue(); if(timesUsed < CONNECTION_USED_MIN) CONNECTION_USED_MIN = timesUsed; if(timesUsed > CONNECTION_USED_MAX) CONNECTION_USED_MAX = timesUsed; } } for(long n : levels.keySet()) { JSONObject context = Helper.findContextData(graph.get("contexts").isArray(), n); JSONArray relationsTo = context.get("relationsTo").isArray(); for (int i = 0; relationsTo != null && i < relationsTo.size(); i++) { long next = (long)relationsTo.get(i).isObject().get("destination").isNumber().doubleValue(); double timesused = relationsTo.get(i).isObject().get("timesUsed").isNumber().doubleValue(); double weight = (timesused - GlobalGraphSettings.CONNECTION_USED_MIN)/(GlobalGraphSettings.CONNECTION_USED_MAX - GlobalGraphSettings.CONNECTION_USED_MIN); if(levels.containsKey(next)) { //draw only connections to nodes w/ levels (e.g. subgraph) DrawableConnection dc = new DrawableConnection(n, next, (long)timesused, weight); concistent = concistent && dc.paint(paper, drawables, levels); // if this returns false there is some inconsistency } } } if(!concistent) { showConsistencyError(); } addGraphSpecificDrawings(); } abstract void addGraphSpecificDrawings(); private void showConsistencyError() { paper.text(paper.getWidth() / 2, paper.getHeight() / 2, "WARNING: the graph is inconsistent").attr("fill", "#f00").attr("font-size", 30); } public void fadeIn() { Rect r1 = paper.rect(0,0,paper.getWidth(),paper.getHeight(),0); r1.attr("fill", "#fff").attr("stroke", "#fff"); r1.animate(Raphael.animation(Attrs.create().opacity(0), 300, "<", new Callback() { public void call(Shape src) { src.hide(); } })); } public void fadeOut(final Callback callback) { Rect r1 = paper.rect(0,0,paper.getWidth(),paper.getHeight(),0); r1.attr("fill", "#fff").attr("stroke", "#fff").attr("opacity", 0); r1.animate(Raphael.animation(Attrs.create().opacity(1), 300, "<", new Callback() { public void call(Shape src) { callback.call(src); } })); } }